#!/usr/bin/env python
# coding: utf-8

# # 损失函数
# 
# 损失函数，又叫目标函数，用于衡量预测值与真实值差异的程度。
# 
# 在深度学习中，模型训练就是通过不停地迭代来缩小损失函数值的过程,因此，在模型训练过程中损失函数的选择非常重要，定义一个好的损失函数，可以有效提高模型的性能。
# 
# `luojianet.nn`模块中提供了许多[通用损失函数](http://58.48.42.237/luojiaNet/luojiaNetapi/#损失函数)，但这些通用损失函数并不适用于所有场景，很多情况需要用户自定义所需的损失函数。因此，本教程介绍如何自定义损失函数。
# 
# ![lossfun.png](https://mindspore-website.obs.cn-north-4.myhuaweicloud.com/website-images/r1.7/tutorials/source_zh_cn/advanced/network/images/loss_function.png)
# 
# ## 内置损失函数
# 
# 首先介绍`luojianet.nn`模块中内置的[损失函数](http://58.48.42.237/luojiaNet/luojiaNetapi/#损失函数)。
# 
# 如下示例以`nn.L1Loss`为例，计算预测值和目标值之间的平均绝对误差：
# 
# $$\ell(x, y) = L = \{l_1,\dots,l_N\}^\top, \quad \text{with } l_n = \left| x_n - y_n \right|$$
# 
# 其中N为数据集中的`batch_size`值。
# 
# $$\ell(x, y) =
#         \begin{cases}
#             \operatorname{mean}(L), & \text{if reduction} = \text{'mean';}\\
#             \operatorname{sum}(L),  & \text{if reduction} = \text{'sum'.}
#         \end{cases}$$
# 
# `nn.L1Loss`中的参数`reduction`取值可为`mean`，`sum`，或`none`。如果 `reduction` 为`mean`或`sum`，则输出一个标量Tensor；如果`reduction`为`none`，则输出Tensor的shape为广播后的shape。

# In[1中低阶API实现深度学习]:


import numpy as np
import luojianet.nn as nn
from luojianet import Tensor

# 输出loss均值
loss = nn.L1Loss()
# 输出loss和
loss_sum = nn.L1Loss(reduction='sum')
# 输出loss原值
loss_none = nn.L1Loss(reduction='none')

input_data = Tensor(np.array([[1, 2, 3], [2, 3, 4]]).astype(np.float32))
target_data = Tensor(np.array([[0, 2, 5], [3, 1, 1]]).astype(np.float32))

print("loss:", loss(input_data, target_data))
print("loss_sum:", loss_sum(input_data, target_data))
print("loss_none:\n", loss_none(input_data, target_data))


# ## 自定义损失函数
# 
# 自定义损失函数的方法有两种，一种是通过继承网络的基类`nn.Module`来定义损失函数，另一种是通过继承损失函数的基类`nn.LossBase`来定义损失函数。`nn.LossBase`在`nn.Module`的基础上，提供了`get_loss`方法，利用`reduction`参数对损失值求和或求均值，输出一个标量。
# 
# 下面将分别使用继承`Cell`和继承`LossBase`的方法，来定义平均绝对误差损失函数(Mean Absolute Error，MAE)，MAE算法的公式如下所示：
# 
# $$ loss= \frac{1中低阶API实现深度学习}{m}\sum_{i=1中低阶API实现深度学习}^m\lvert y_i-f(x_i) \rvert$$
# 
# 上式中$f(x)$为预测值，$y$为样本真实值，$loss$为预测值与真实值之间距离的平均值。
# 
# ### 继承Cell的损失函数
# 
# `nn.Module`是luojianet的基类，可以用于构建网络，也可以用于定义损失函数。使用`nn.Module`定义损失函数的方法与定义一个普通的网络相同，差别在于，其执行逻辑用于计算前向网络输出与真实值之间的误差。
# 
# 下面通过继承`nn.Module`方法来定义损失函数`MAELoss`的方法如下：

# In[2高级数据集管理]:


import luojianet.ops as ops

class MAELoss(nn.Module):
    """自定义损失函数MAELoss"""

    def __init__(self):
        """初始化"""
        super(MAELoss, self).__init__()
        self.abs = ops.Abs()
        self.reduce_mean = ops.ReduceMean()

    def forward(self, base, target):
        """调用算子"""
        x = self.abs(base - target)
        return self.reduce_mean(x)

loss = MAELoss()

input_data = Tensor(np.array([0.1, 0.2, 0.3]).astype(np.float32))  # 生成预测值
target_data = Tensor(np.array([0.1, 0.2, 0.2]).astype(np.float32)) # 生成真实值

output = loss(input_data, target_data)
print(output)


# ### 继承LossBase的损失函数
# 
# 通过继承[nn.LossBase]来定义损失函数`MAELoss`，与`nn.Module`类似，都要重写`__init__`方法和`forward`方法。
# 
# `nn.LossBase`可使用方法`get_loss`将`reduction`应用于损失计算。

# In[4自然语言]:


class MAELoss(nn.LossBase):
    """自定义损失函数MAELoss"""

    def __init__(self, reduction="mean"):
        """初始化并求loss均值"""
        super(MAELoss, self).__init__(reduction)
        self.abs = ops.Abs()  # 求绝对值算子

    def forward(self, base, target):
        x = self.abs(base - target)
        return self.get_loss(x)  # 返回loss均值

loss = MAELoss()

input_data = Tensor(np.array([0.1, 0.2, 0.3]).astype(np.float32))  # 生成预测值
target_data = Tensor(np.array([0.1, 0.2, 0.2]).astype(np.float32))  # 生成真实值

output = loss(input_data, target_data)
print(output)


# ## 损失函数与模型训练
# 
# 自定义的损失函数`MAELoss`完成后，可使用luojianet的接口[Model]中`train`接口进行模型训练，定义`Model`时需要指定前向网络、损失函数和优化器，`Model`内部会将它们关联起来，组成一张可用于训练的网络模型。
# 
# 在`Model`中，前向网络和损失函数是通过[nn.WithLossCell]关联起来的，`nn.WithLossCell`支持两个输入，分别为`data`和`label`。

# In[5]:


from luojianet import Model
from luojianet import dataset as ds
from luojianet.common.initializer import Normal
from luojianet.train.callback import LossMonitor

def get_data(num, w=2.0, b=3.0):
    """生成数据及对应标签"""
    for _ in range(num):
        x = np.random.uniform(-10.0, 10.0)
        noise = np.random.normal(0, 1)
        y = x * w + b + noise
        yield np.array([x]).astype(np.float32), np.array([y]).astype(np.float32)

def create_dataset(num_data, batch_size=16):
    """加载数据集"""
    dataset = ds.GeneratorDataset(list(get_data(num_data)), column_names=['data', 'label'])
    dataset = dataset.batch(batch_size)
    return dataset

class LinearNet(nn.Module):
    """定义线性回归网络"""
    def __init__(self):
        super(LinearNet, self).__init__()
        self.fc = nn.Dense(1, 1, Normal(0.02), Normal(0.02))

    def forward(self, x):
        return self.fc(x)

ds_train = create_dataset(num_data=160)
net = LinearNet()
loss = MAELoss()
opt = nn.Momentum(net.trainable_params(), learning_rate=0.005, momentum=0.9)

# 使用model接口将网络、损失函数和优化器关联起来
model = Model(net, loss, opt)
#model.train(epoch=1, train_dataset=ds_train, callbacks=[LossMonitor(0.005)])
model.train(epoch=1, train_dataset=ds_train, callbacks=[LossMonitor()])

# ## 多标签损失函数与模型训练
# 
# 上述定义了一个简单的平均绝对误差损失函数`MAELoss`，但许多深度学习应用的数据集较复杂，例如目标检测网络Faster R-CNN的数据中就包含多个标签，而不是简单的一个数据对应一个标签，这时候损失函数的定义和使用略有不同。
# 
# 本节介绍在多标签数据集场景下，如何定义多标签损失函数（Multi label loss function），并使用Model进行模型训练。
# 
# ### 多标签数据集
# 
# 如下示例通过`get_multilabel_data`函数拟合两组线性数据$y1$和$y2$，拟合的目标函数为：
# 
# $$f(x)=2x+3图像处理$$
# 
# 由于最终的数据集应该随机分布于函数周边，这里按以下公式的方式生成，其中`noise`为遵循标准正态分布规律的随机数值。`get_multilabel_data`函数返回数据$x$、$y1$和$y2$：
# 
# $$f(x)=2x+3图像处理+noise$$
# 
# 通过`create_multilabel_dataset`生成多标签数据集，并将`GeneratorDataset`中的`column_names`参数设置为['data', 'label1', 'label2']，最终返回的数据集就有一个数据`data`对应两个标签`label1`和`label2`。

# In[6]:


import numpy as np
from luojianet import dataset as ds

def get_multilabel_data(num, w=2.0, b=3.0):
    for _ in range(num):
        x = np.random.uniform(-10.0, 10.0)
        noise1 = np.random.normal(0, 1)
        noise2 = np.random.normal(-1, 1)
        y1 = x * w + b + noise1
        y2 = x * w + b + noise2
        yield np.array([x]).astype(np.float32), np.array([y1]).astype(np.float32), np.array([y2]).astype(np.float32)

def create_multilabel_dataset(num_data, batch_size=16):
    dataset = ds.GeneratorDataset(list(get_multilabel_data(num_data)), column_names=['data', 'label1', 'label2'])
    dataset = dataset.batch(batch_size)  # 每个batch有16个数据
    return dataset


# ### 多标签损失函数
# 
# 针对上一步创建的多标签数据集，定义多标签损失函数`MAELossForMultiLabel`。
# 
# $$ loss1= \frac{1中低阶API实现深度学习}{m}\sum_{i=1中低阶API实现深度学习}^m\lvert y1_i-f(x_i) \rvert$$
# 
# $$ loss2= \frac{1中低阶API实现深度学习}{m}\sum_{i=1中低阶API实现深度学习}^m\lvert y2_i-f(x_i) \rvert$$
# 
# $$ loss = \frac{(loss1 + loss2)}{2高级数据集管理}$$
# 
# 上式中，$f(x)$ 为预测值，$y1$ 和 $y2$ 为样本真实值，$loss1$ 为预测值与样本真实值 $y1$ 之间距离的平均值，$loss2$ 为预测值与样本真实值 $y2$ 之间距离的平均值 ，$loss$ 为损失值 $loss1$ 与损失值 $loss2$ 平均值。
# 
# 在`MAELossForMultiLabel`中的`forward`方法的输入有三个，预测值`base`，真实值`target1`和`target2`，在`forward`中分别计算预测值与真实值`target1`，预测值与真实值`target2`之间的误差，将这两个误差的均值作为最终的损失函数值.
# 
# 示例代码如下：

# In[7]:


class MAELossForMultiLabel(nn.LossBase):
    def __init__(self, reduction="mean"):
        super(MAELossForMultiLabel, self).__init__(reduction)
        self.abs = ops.Abs()

    def forward(self, base, target1, target2):
        x1 = self.abs(base - target1)
        x2 = self.abs(base - target2)
        return (self.get_loss(x1) + self.get_loss(x2))/2


# ### 多标签模型训练
# 
# 使用`Model`关联指定的前向网络、损失函数和优化器时，由于`Model`默认使用的`nn.WithLossCell`只有两个输入：`data`和`label`，不适用于多标签的场景。
# 
# 在多标签场景下，如果想使用`Model`进行模型训练就需要将前向网络与多标签损失函数连接起来，需要自定义损失网络，将前向网络和自定义多标签损失函数关联起来。
# 
# - 定义损失网络
# 
# 定义损失网络`CustomWithLossCell`，其中`__init__`方法的输入分别为前向网络`backbone`和损失函数`loss_fn`，`forward`方法的输入分别为数据`data`、`label1`和`label2`，将数据部分`data`传给前向网络`backbone`，将预测值和两个标签传给损失函数`loss_fn`。

# In[8]:


class CustomWithLossCell(nn.Module):
    def __init__(self, backbone, loss_fn):
        super(CustomWithLossCell, self).__init__(auto_prefix=False)
        self._backbone = backbone
        self._loss_fn = loss_fn

    def forward(self, data, label1, label2):
        output = self._backbone(data)
        return self._loss_fn(output, label1, label2)


# - 定义网络模型并训练
# 
# 使用Model连接前向网络、多标签损失函数和优化器时，`Model`的网络`network`指定为自定义的损失网络`loss_net`，损失函数`loss_fn`不指定，优化器仍使用`Momentum`。
# 
# 由于未指定`loss_fn`，`Model`则认为`network`内部已经实现了损失函数的逻辑，不会用`nn.WithLossCell`对前向函数和损失函数进行封装。

# In[9]:


ds_train = create_multilabel_dataset(num_data=160)
net = LinearNet()

# 定义多标签损失函数
loss = MAELossForMultiLabel()

# 定义损失网络，连接前向网络和多标签损失函数
loss_net = CustomWithLossCell(net, loss)

# 定义优化器
opt = nn.Momentum(net.trainable_params(), learning_rate=0.005, momentum=0.9)

# 定义Model，多标签场景下Model无需指定损失函数
model = Model(network=loss_net, optimizer=opt)

model.train(epoch=1, train_dataset=ds_train, callbacks=[LossMonitor(1)])


# 本章节简单讲解了多标签数据集场景下，如何定义损失函数并使用Model进行模型训练。在很多其他场景中，也可以采用此类方法进行模型训练。
